import fs from "fs-extra";
import fg from "fast-glob";
import path from "path";
import { ClassStaticPropertyTypes, Node, Project, ReferenceFindableNode, SyntaxKind } from "ts-morph";
import { eitherExists } from "./vendor.js";
import { toolchain } from "../toolchain.js";

export class CodeStriper {
    public async removeUseless(): Promise<void> {
        const project = new Project({
            tsConfigFilePath: path.join(toolchain.opts.projectRoot, 'TsScripts/tsconfig.json')
        });
        // await this.warnEmptyMethods(project);
        await this.removeStaticProperties(project, 'UIPathData');
        await this.removeProperties(project, 'AllAssetAtlas');
        // await this.processGetCfg(project);
        // await this.removeUnnecessaryCodes(project);
        // await this.removeEmptyFolders();
    }

    private async removeStaticProperties(project: Project, srcName: string): Promise<void> {
        const tsRoot = path.join(toolchain.opts.projectRoot, 'TsScripts');
        const files = await fg(`**/${srcName}.ts`, { cwd: tsRoot });
        if (files.length == 0) {
            console.error('no src file:', srcName);
            return;
        }
        const updFile = path.join(tsRoot, files[0]);
        const updSrc = project.getSourceFile(updFile)!;
        const updCls = updSrc.getClass(srcName)!;
        let cnt = 0;
        updCls.getStaticProperties().forEach((value: ClassStaticPropertyTypes) => {
            if (!this.isNodeReferedOutside(value, [updFile])) {
                value.remove();
                cnt++;
            }
        });

        updSrc.save();
        console.log(`${cnt} useless properties of ${srcName} removed.`);
    }

    private async removeProperties(project: Project, srcName: string): Promise<void> {
        const tsRoot = path.join(toolchain.opts.projectRoot, 'TsScripts');
        const files = await fg(`**/${srcName}.ts`, { cwd: tsRoot });
        if (files.length == 0) {
            console.error('no src file:', srcName);
            return;
        }
        const updFile = path.join(tsRoot, files[0]);
        const updSrc = project.getSourceFile(updFile)!;
        const updCls = updSrc.getClass(srcName)!;
        let cnt = 0;
        updCls.getProperties().forEach((value: ClassStaticPropertyTypes) => {
            if (!this.isNodeReferedOutside(value, [updFile])) {
                value.remove();
                cnt++;
            }
        });

        updSrc.save();
        console.log(`${cnt} useless properties of ${srcName} removed.`);
    }

    private isNodeReferedOutside(node: ReferenceFindableNode & Node, excludes: string[]): boolean {
        const refs = node.findReferencesAsNodes();
        if (refs.length == 0) return false;

        let recursiveCnt = 0;
        for (const ref of refs) {
            if (node.getSourceFile() != ref.getSourceFile()) {
                return true;
            }
            let p = ref.getParent();
            while (p) {
                if (p == node) {
                    recursiveCnt++;
                    break;
                }
                p = p.getParent();
            }
        }
        return recursiveCnt < refs.length;
    }

    private async processGetCfg(project: Project): Promise<void> {
        // 收集所有json
        const jsonRoot = eitherExists(path.join(toolchain.opts.projectRoot, 'Assets/AssetSources/data'), path.join(toolchain.opts.projectRoot, 'assets/data'));
        if (!jsonRoot) {
            console.error('no json root');
            return;
        }
        const allJson = await fg('*.json', { cwd: jsonRoot });
        const jsonMap: { [name: string]: true } = {};
        for (const j of allJson) {
            const jp = path.parse(j);
            jsonMap[jp.name] = true;
        }

        const tsRoot = path.join(toolchain.opts.projectRoot, 'TsScripts');
        const tsFiles = await fg('**/*.ts', { cwd: tsRoot });
        for (const ts of tsFiles) {
            const file = path.join(tsRoot, ts);
            const content = await fs.readFile(file, 'utf-8');
            const lines = content.split(/\r?\n/);
            let modified = false;
            for (let i = 0, len = lines.length; i < len; i++) {
                const line = lines[i];
                if (line.match(/^\s*\/\//)) continue;
                const rst = line.match(/\.getCfg(?:<[\.\w]+>)?\((['|"])(?:data\/)?(\w+)(?:\.json+)?\1\)/);
                if (rst != null) {
                    const desierJson = rst[2];
                    if (!jsonMap[desierJson]) {
                        console.log('json not exists:', desierJson);
                        lines[i] = line.replace(desierJson, desierJson + '//');
                        modified = true;
                    }
                }
            }
            if (modified) {
                await fs.writeFile(file, lines.join('\n'), 'utf-8');
            }

        }
    }

    private async warnEmptyMethods(project: Project): Promise<void> {
        const tsRoot = path.join(toolchain.opts.projectRoot, 'TsScripts');
        const tsFiles = await fg('**/*.ts', { cwd: tsRoot, ignore: ['**/*.d.ts'] });
        let modified = false;
        for (let i = 0, len = tsFiles.length; i < len; i++) {
            const ts = tsFiles[i];
            // if (!ts.endsWith('AvatarChanger.ts')) continue;
            const file = path.join(tsRoot, ts);
            const src = project.getSourceFile(file)!;
            if (!src) {
                console.error('SourceFile not exists:', file);
                continue;
            }
            const classes = src.getClasses();
            for (const cls of classes) {
                if (cls.isAbstract()) continue;
                const mtds = cls.getMethods();
                for (const m of mtds) {
                    const b = m.getBody();
                    if (b?.isKind(SyntaxKind.Block) && b.getStatements().length == 0) {
                        const mn = m.getName();
                        if (mn == 'onCfgReady' || mn == 'onTipMarkChange' || mn == 'onContainerChange' || mn == 'onCurrencyChange') {
                            modified = true;
                            m.remove();
                        } else {
                            console.warn('empty method: ', file, cls.getName(), mn);                            
                        }
                    }
                }
            }
        }
        await project.save();
    }

    private async removeUnnecessaryCodes(project: Project): Promise<void> {
        await this.removeUnusedTsFiles(project);
        await this.removeUnusedMethods(project);
        await this.removeUnnecessaryOverrides();

        // 删除没用的identifier
        const tsRoot = path.join(toolchain.opts.projectRoot, 'TsScripts');
        const ruis = toolchain.opts.cfg?.codeCfg?.reserveUnusedIdentifiers;
        const ignore = ['**/*.d.ts'];
        if (ruis) {
            ignore.push(...ruis);
        }
        const tss = await fg('**/*.ts', { cwd: tsRoot, ignore });
        for (const ts of tss) {
            const tsf = path.join(tsRoot, ts);
            const src = project.getSourceFileOrThrow(tsf);
            src.fixUnusedIdentifiers();
        }
        await project.save();
    }

    private async removeUnusedTsFiles(project: Project): Promise<void> {
        console.log('removing unused ts files...');
        const tsRoot = path.join(toolchain.opts.projectRoot, 'TsScripts');
        const tsFiles = await fg('**/*.ts', { cwd: tsRoot, ignore: ['**/*.d.ts', 'root.ts', 'minigame.ts', 'uts/*.ts', '**/SysDefines.ts'] });
        for (const ts of tsFiles) {
            const file = path.join(tsRoot, ts);
            const src = project.getSourceFile(file)!;
            if (!src) {
                console.error('SourceFile not exists:', file);
                continue;
            }
            if (src.getReferencingSourceFiles().length == 0) {
                console.log('delete:', ts);
                await src.deleteImmediately();
            }
        }
    }

    private async removeUnusedMethods(project: Project): Promise<void> {
        console.log('removing unused methods...');
        const tsRoot = path.join(toolchain.opts.projectRoot, 'TsScripts');
        const tsFiles = await fg('**/*.ts', { cwd: tsRoot, ignore: ['**/*.d.ts'] });
        let modified = false;
        for (let i = 1412, len = tsFiles.length; i < len; i++) {
            const ts = tsFiles[i];
            // if (!ts.endsWith('AvatarChanger.ts')) continue;
            console.log(`${i}/${len}`, ts);
            const file = path.join(tsRoot, ts);
            const src = project.getSourceFile(file)!;
            if (!src) {
                console.error('SourceFile not exists:', file);
                continue;
            }
            const classes = src.getClasses();
            for (const cls of classes) {
                const clsRefs = cls.findReferencesAsNodes();
                let isClsUseless = true;
                for (let i = 0, len = clsRefs.length; i < len; i++) {
                    const r = clsRefs[i];
                    if (r.isKind(SyntaxKind.Identifier) && r.getParentIf((parent) =>Boolean(!parent?.isKind(SyntaxKind.ImportSpecifier) && !parent?.isKind(SyntaxKind.ClassDeclaration)))) {
                        isClsUseless = false;
                        break;
                    }
                }

                if (isClsUseless) {
                    console.log('useless class: ', cls.getName());
                    cls.remove();
                    modified = true;
                    continue;
                }

                const methods = cls.getMembers();
                for (const mtd of methods) {
                    if (mtd.isKind(SyntaxKind.ClassStaticBlockDeclaration) || mtd.isKind(SyntaxKind.Constructor)) continue;
                    if (!this.isNodeReferedOutside(mtd, [])) {
                        console.log('remove:', mtd.getName());
                        mtd.remove();
                        modified = true;
                    // } else {
                    //     console.log('keep:', mtd.getName());
                    }
                }
            }
            let isSrcUseless = true;
            src.forEachChild((n) => {
                if (!n.isKind(SyntaxKind.EndOfFileToken)) {
                    isSrcUseless = false;
                }
            });
            if (isSrcUseless) {
                // console.log('delete src:', ts);
                // await fs.unlink(file);
                await src.deleteImmediately();
            } else if (modified) {
                await src.save();
            }
        }
    }

    private async removeUnnecessaryOverrides(): Promise<void> {
        console.log('removing unnecessary overrides...');
        const tsRoot = path.join(toolchain.opts.projectRoot, 'TsScripts');
        const tsFiles = await fg('**/*.ts', { cwd: tsRoot, ignore: ['**/*.d.ts'] });
        for (const ts of tsFiles) {
            const file = path.join(tsRoot, ts);
            const content = await fs.readFile(file, 'utf-8');
            const lines = content.split(/\r?\n/);
            let deleteLines: number[] = [], inForm = false, funcStartLine = -1, funcBodyExists = false;
            for (let i = 0, len = lines.length; i < len; i++) {
                const line = lines[i];
                if (funcStartLine > 0) {
                    if (line.match(/^    \}/)) {
                        if (!funcBodyExists) {
                            for (let j = funcStartLine; j <= i; j++) {
                                deleteLines.push(j);
                            }
                        }
                        funcStartLine = -1;
                    } else {
                        if (!line.match(/^\s*\/\//) && line.match(/\S+/) && !line.match('super\.')) {
                            funcBodyExists = true;
                        }
                    }
                } else if (inForm) {
                    if (line.match(/^\}/)) {
                        inForm = false;
                    } else {
                        if (line.match(/\s+(?:initElements|initListeners|onClose|onOpen|open)\(\)/)) {
                            funcStartLine = i;
                            funcBodyExists = false;
                        }
                    }
                } else if (line.match(/class \w+ extends (?:CommonForm|TabForm|TabSubForm)/)) {
                    inForm = true;
                }
            }
            if (deleteLines.length > 0) {
                console.log('Unnecessary code removed:', ts);
                await fs.writeFile(file, lines.filter((v, i) => !deleteLines.includes(i)).join('\n'), 'utf-8');
            }
        }
    }

    private async removeEmptyFolders(): Promise<void> {
        console.log('removing empty folders...');
        const tsRoot = path.join(toolchain.opts.projectRoot, 'TsScripts');
        const folders = await fg('**/*', { cwd: tsRoot, onlyDirectories: true });
        folders.sort((a, b) => b.length - a.length);
        for (const f of folders) {
            const folder = path.join(tsRoot, f);
            const subs = await fg('**/*', { cwd: folder });
            if (subs.length == 0) {
                await fs.rmdir(folder);
            }
        }
    }

    /**查找潜在未使用的CommonForm */
    async findSuspectedUselessUI(): Promise<void> {
        const tsRoot = path.join(toolchain.opts.projectRoot, 'TsScripts');
        const tsFiles = await fg('**/*.ts', { cwd: tsRoot });

        // 再查找所有的CommonForm、TabForm
        const project = new Project({
            tsConfigFilePath: path.join(tsRoot, 'tsconfig.json')
        });

        // 先查找所有调用了createForm的面板
        const parentMap: Record<string, string[]> = {}, 
        subFormMap: Record<string, string[]> = {}, 
        dirOpenedForms: Record<string, true> = {}, 
        commonForms: string[] = [];

        for (const f of tsFiles) {
            const tsf = path.join(tsRoot, f);
            const content = await fs.readFile(tsf, 'utf-8');
            const lines = content.split(/\r?\n/).filter((v) => !v.trimStart().startsWith('//'));
            const mchs = lines.join('\n').matchAll(/(?:createForm|createChildForm|dominate)(?:<\w+>)?\((\w+)/g);
            for (const mch of mchs) {
                dirOpenedForms[mch[1]] = true;
            }

            const src = project.getSourceFile(tsf)!;
            if (!src) {
                console.error('SourceFile not exists:', tsf);
                continue;
            }
            const classes = src.getClasses();
            for (const cls of classes) {
                const parents: string[] = [];
                let isCommonForm = false, isTabForm = false;
                let bs = cls.getBaseClass();
                while (bs != null) {
                    const bsn = bs.getName();
                    if (bsn) parents.push(bsn);
                    if (bsn == 'TabForm') {
                        isTabForm = true;
                    }
                    if (bsn == 'CommonForm') {
                        isCommonForm = true;
                        break;
                    }
                    bs = bs.getBaseClass();
                }

                if (isCommonForm) {
                    const cn = cls.getName();
                    if (cn) {
                        commonForms.push(cn);
                        parentMap[cn] = parents;
                    }
                }
                if (isTabForm) {
                    const cn = cls.getName();
                    if (cn) {
                        // 收集所属子页签
                        const ctors = cls.getConstructors();
                        for (const ctor of ctors) {
                            const ctorBody = ctor.getBody();
                            if (ctorBody) {
                                ctorBody.forEachChild((n) => {
                                    if (n.isKind(SyntaxKind.ExpressionStatement)) {
                                        const exp0 = n.getExpression();
                                        if (exp0.isKind(SyntaxKind.CallExpression)) {
                                            const exp = exp0.getExpression();
                                            if (exp.isKind(SyntaxKind.SuperKeyword)) {
                                                const subForms: string[] = [];
                                                const args = exp0.getArguments();
                                                for (let i = 1, len = args.length; i < len; i++) {
                                                    subForms.push(args[i].getText());
                                                }
                                                subFormMap[cn] = subForms;
                                            }
                                        }
                                    }
                                });
                            }
                        }
                    }
                }
            }
        }

        const openedMap: Record<string, true> = {};
        const markOpened = (f: string) => {
            if (openedMap[f]) return;
            openedMap[f] = true;
            // 标记所有subform
            const subs = subFormMap[f];
            if (subs) {
                subs.forEach((v) => {
                    markOpened(v);
                })
            }
            // 标记所有父类
            const parents = parentMap[f];
            if (parents) {
                parents.forEach((v) => {
                    markOpened(v);
                })
            }
        }

        for (const f of commonForms) {
            if (dirOpenedForms[f]) {
                markOpened(f);
            }
        }

        const uselessForms: string[] = [];
        for (const f of commonForms) {
            if (!openedMap[f]) {
                uselessForms.push(f);
            }
        }
        
        console.log('useless forms: ', uselessForms.length);
        console.log('-------------------------');
        uselessForms.forEach((v) => console.log(v));
    }

    async showCodeHint(): Promise<void> {
        const tsRoot = path.join(toolchain.opts.projectRoot, 'TsScripts');
        const tsFiles = await fg('**/*.ts', { cwd: tsRoot });

        // 再查找所有的CommonForm、TabForm
        const project = new Project({
            tsConfigFilePath: path.join(tsRoot, 'tsconfig.json')
        });

        for (const f of tsFiles) {
            const tsf = path.join(tsRoot, f);
            const src = project.getSourceFile(tsf)!;
            if (!src) {
                console.error('SourceFile not exists:', tsf);
                continue;
            }
            const classes = src.getClasses();
            for (const cls of classes) {
                if (cls.getMethods().length == 0 && cls.getStaticMethods().length == 0 && cls.getGetAccessors().length == 0 && cls.getSetAccessors().length == 0 && cls.getConstructors().length == 0) {
                    console.log(f, cls.getName());
                }
            }
        }
    }
}
